Introduction

In this blog post, I want to show you a graph-based way to split up a class into several independent ones. We take a small example class from Michael Feathers' book "Working effectively with legacy code" and use Neo4j's Awesome Procedures On Cypher (APOC).

Hint: To run the notebook version of this blog post, you need to install the ipython-cypher extension.

class Reservation {

    private int duration;
    private int dailyRate;
    private Date date;
    private Customer customer;
    private List fees = new ArrayList();

    public Reservation(Customer customer, int duration, int dailyRate, Date date) {
        this.customer = customer;
        this.duration = duration;
        this.dailyRate = dailyRate;
        this.date = date;
    }

    public void extend(int additionalDays) {
        duration += additionalDays;
    }

    public void extendForWeek() {
        int weekRemainder = RentalCalendar.weekRemainderFor(date);
        final int DAYS_PER_WEEK = 7;
        extend(weekRemainder);
        dailyRate = RateCalculator.computeWeekly(
                customer.getRateCode()) / DAYS_PER_WEEK;
    }

    public void addFee(FeeRider rider) {
        fees.add(rider);
    }

    int getAdditionalFees() {
        int total = 0;
        for (Iterator it = fees.iterator(); it.hasNext(); ) {
            total += ((FeeRider) (it.next())).getAmount();

        }
        return total;
    }

    int getPrincipalFee() {
        return dailyRate * RateCalculator.rateBase(customer) * duration;
    }

    public int getTotalFee() {
        return getPrincipalFee() + getAdditionalFees();
    }
}

In [62]:
%load_ext cypher


The cypher extension is already loaded. To reload it, use:
  %reload_ext cypher

In [63]:
%%cypher
MATCH
    ()-[u:USES]->(),
    (n:NewClass)-[s:SHOULD_DECLARE]->()
DELETE u,s,n


2 nodes deleted.
24 relationship deleted.
Out[63]:

In [64]:
%%cypher
MATCH
    (c:Class {name : "Reservation"}),
    (c)-[:DECLARES]->(m:Method),
    (c)-[:DECLARES]->(f:Field),
    (m)-[:READS|WRITES]->(f)
WHERE NOT (m:Constructor)
MERGE (m)-[u:USES]->(f)
RETURN m.name as method, type(u) as relType, f.name as field


9 relationships created.
Out[64]:
method relType field
getAdditionalFees USES fees
addFee USES fees
getPrincipalFee USES customer
extendForWeek USES customer
getPrincipalFee USES duration
extend USES duration
extend USES duration
extendForWeek USES date
getPrincipalFee USES dailyRate
extendForWeek USES dailyRate

In [65]:
%%cypher
MATCH
    (c:Class {name : "Reservation"}),
    (c)-[:DECLARES]->(m:Method),
    (c)-[:DECLARES]->(m2:Method),
    (m)-[:INVOKES]->(m2:Method)
WHERE NOT (m:Constructor)
MERGE (m)-[u:USES]->(m2)
RETURN m.name as caller, type(u) as relType, m2.name as callee


3 relationships created.
Out[65]:
caller relType callee
extendForWeek USES extend
getTotalFee USES getPrincipalFee
getTotalFee USES getAdditionalFees

In [66]:
%%cypher
MATCH (m)-[u:USES]->(f)
WITH m, COUNT(u) as weight
SET m.weight = weight
RETURN m.name as method, weight


6 properties set.
Out[66]:
method weight
getPrincipalFee 3
getAdditionalFees 1
getTotalFee 2
extendForWeek 4
addFee 1
extend 1

In [67]:
%%cypher
MATCH (m)-[u:USES]->(f:Field)
WITH f, COUNT(u) as weight
SET f.weight = weight
RETURN f.name as field, weight


5 properties set.
Out[67]:
field weight
duration 2
customer 2
date 1
fees 2
dailyRate 2

In [68]:
%%cypher
MATCH (m)-[u:USES]->(m2:Method)
WITH m2, COUNT(u) as weight
SET m2.weight = weight
RETURN m2.name as callee, weight


3 properties set.
Out[68]:
callee weight
getPrincipalFee 1
getAdditionalFees 1
extend 1

Now we have to move the information of the called items to the relationship.


In [69]:
%%cypher
MATCH (caller)-[r:USES]->(callee)
SET r.weight = callee.weight
RETURN count(r)


12 properties set.
Out[69]:
count(r)
12

In [70]:
%%cypher
CALL apoc.algo.community(25,null,'group','USES','OUTGOING','weight',10000)


0 rows affected.
Out[70]:

In [71]:
%%cypher
MATCH (m:Method)-[:USES]->(f:Field)<-[:USES]-(m2:Method)
WHERE m.group <> m2.group 
WITH m.group as newGroupId, m2.group as oldGroupId
MATCH (n:Method) WHERE n.group = oldGroupId
SET n.group = newGroupId+oldGroupId
SET n.merged = true
RETURN DISTINCT(n.name), n.group;


16 properties set.
Out[71]:
(n.name) n.group
extendForWeek 83
getTotalFee 83
extend 83
getPrincipalFee 83

In [72]:
%%cypher
MATCH (m:Method)-[:USES]->(:Field)
WHERE NOT EXISTS(m.merged)
WITH m, m.group as groupId
SET m.merged = false
RETURN m.name, m.group;


0 rows affected.
Out[72]:
m.name m.group

In [73]:
%%cypher
MATCH (m:Method)-[:USES]->(f)
WHERE (NOT EXISTS(m.merge) OR m.merge = False)
MERGE (c:NewClass { name: m.group})
MERGE (c)-[:SHOULD_DECLARE]->(m)
MERGE (c)-[:SHOULD_DECLARE]->(f)
RETURN c.name as newClass, m.name as method, f.name as field


2 nodes created.
2 properties set.
12 relationships created.
2 labels added.
Out[73]:
newClass method field
83 getTotalFee getAdditionalFees
83 getTotalFee getPrincipalFee
83 extend duration
83 extendForWeek extend
83 extendForWeek customer
83 extendForWeek dailyRate
83 extendForWeek date
45 addFee fees
45 getAdditionalFees fees
83 getPrincipalFee duration
83 getPrincipalFee customer
83 getPrincipalFee dailyRate

In [ ]: